#1. Object Safety 的基本要求
要让 trait 可以用作 trait object (dyn Trait),必须满足 object safety 规则:
trait ObjectSafe {
fn method(&self) -> String; // ✅ 有 receiver
// fn generic<T>(&self) -> T; // ❌ 不能有泛型参数
// fn returns_self(&self) -> Self; // ❌ 不能返回 Self
// const CONST: i32; // ❌ 不能有关联常量
}核心原理:动态派发通过 vtable(虚函数表)实现,vtable 必须在编译时确定每个方法的确切签名。
#2. Async 函数的实际机制
// 你写的代码:
async fn foo(&self) -> String {
tokio::time::sleep(Duration::from_millis(100)).await;
"hello".to_string()
}
// 编译器实际生成的:
fn foo(&self) -> impl Future<Output = String> + '_ {
// 返回一个编译器生成的唯一状态机类型
// 比如:FooFuture_A_12345 { state: ..., self_ref: ... }
}关键问题:每个 async fn 都生成一个唯一的、不同的状态机类型。
#3. 冲突的根本原因
trait AsyncTrait {
async fn method(&self) -> String;
}
struct ServiceA;
struct ServiceB;
impl AsyncTrait for ServiceA {
async fn method(&self) -> String {
"A".to_string() // 立即返回
}
}
impl AsyncTrait for ServiceB {
async fn method(&self) -> String {
tokio::time::sleep(Duration::from_millis(100)).await;
"B".to_string() // 需要异步等待
}
}编译器为这两个实现生成完全不同的 Future 类型:
// ServiceA 生成类似这样的类型:
struct ServiceAMethodFuture {
// 立即完成,可能只需要一个状态位
state: u8,
}
// ServiceB 生成类似这样的类型:
struct ServiceBMethodFuture {
// 需要存储 Timer 状态和更复杂的状态机
timer: Option<tokio::time::Sleep>,
state: ComplexState,
// ... 更多字段
}#4. Vtable 无法统一
当尝试创建 dyn AsyncTrait 时:
// 这样做会失败:
let services: Vec<Box<dyn AsyncTrait>> = vec![
Box::new(ServiceA),
Box::new(ServiceB),
];
// 因为 vtable 不知道 method() 应该返回什么类型:
// ServiceA::method 返回 ServiceAMethodFuture
// ServiceB::method 返回 ServiceBMethodFuture
// 这两个类型完全不兼容!Vtable 需要统一的方法签名,但 impl Future<Output = String> 对每个实现都是不同的具体类型。
#5. 解决方案的原理
// 方案1:Box<dyn Future> 类型擦除
trait CoreService {
fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}
impl CoreService for ServiceA {
fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
Box::pin(async { "A".to_string() })
// ^^^^^^^^^^^^^^^^^^^^^^^^
// 不同的Future类型被擦除到相同的trait object
}
}
impl CoreService for ServiceB {
fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
Box::pin(async {
tokio::time::sleep(Duration::from_millis(100)).await;
"B".to_string()
})
// ^^^^^^^^^^^^
// 也被擦除到相同的trait object
}
}现在 vtable 可以统一了:
// Vtable 中的方法签名:
fn method(*const (), ...) -> Pin<Box<dyn Future<Output = String> + Send>>;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 所有实现都返回相同的类型!#6. 为什么 async_trait crate 能工作?
async_trait 本质上就是自动做了我们手动做的事情:
#[async_trait]
trait AsyncTrait {
async fn method(&self) -> String;
}
// 展开后变成:
trait AsyncTrait {
fn method(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}总结
根本原因:Rust 的类型系统要求 trait objects 的所有方法在编译时有确定的、统一的签名,但 async fn 为每个实现生成不同的 Future 类型,违反了这个要求。
解决思路:通过类型擦除(Box<dyn Future>)将不同的 Future 类型统一到相同的接口后面,让 vtable 可以工作。